/*
 * Copyright (C) 2014 luke.
 * 
 * This file is part of Silvie.
 * 
 * Silvie is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Silvie is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Silvie.  If not, see <http://www.gnu.org/licenses/>.
 */

package silvie.gui.filetree;

import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

/**
 * A TreeModel implementation for representing the Silver data directory. It can
 * force the view to update itself (when files have changed) through the
 * {@link #fireTreeStructureChanged(Path)} method.
 * 
 * @author luke
 */
public class FileTreeModel implements TreeModel {
	private FileTreeNode root;
	private List<TreeModelListener> listeners;
	private Map<Path, FileTreeNode[]> childCache;

	/**
	 * FileTreeModel constructor.
	 * 
	 * @param dataDir the root of the tree (Silver's "data" directory).
	 */
	public FileTreeModel(File dataDir) {
		this.root = new FileTreeNode(dataDir);
		this.listeners = new ArrayList<TreeModelListener>();
		this.childCache = new HashMap<Path, FileTreeNode[]>();
	}

	@Override
	public void addTreeModelListener(TreeModelListener listener) {
		this.listeners.add(listener);
	}

	@Override
	public Object getChild(Object parent, int index) {
		return getChildren((FileTreeNode) parent)[index];
	}

	@Override
	public int getChildCount(Object parent) {
		return getChildren((FileTreeNode) parent).length;
	}

	@Override
	public int getIndexOfChild(Object parent, Object child) {
		FileTreeNode[] children = getChildren((FileTreeNode) parent);
		for (int i = 0; i < children.length; i++) {
			if (((FileTreeNode) child).equals(children[i])) {
				return i;
			}
		}
		return -1;
	}

	@Override
	public Object getRoot() {
		return this.root;
	}

	@Override
	public boolean isLeaf(Object node) {
		return !((FileTreeNode) node).isDirectory();
	}

	@Override
	public void removeTreeModelListener(TreeModelListener listener) {
		this.listeners.remove(listener);
	}

	@Override
	public void valueForPathChanged(TreePath path, Object value) {}

	/**
	 * Returns a sorted FileTreeNode array for the contents of the directory
	 * represented by {@code parent}.
	 * 
	 * <p>
	 * The returned contents are cached so any subsequent call to this method
	 * with the same {@code parent} will use the cache. The cache is cleared
	 * when {@code parent} represents a different file from the {@code parent}
	 * of the previous call.
	 * 
	 * @param parent the FileTreeNode representing the directory whose contents
	 *        are returned.
	 * @return a FileTreeNode array for the contents of the directory
	 *         represented by {@code parent}.
	 */
	private FileTreeNode[] getChildren(FileTreeNode parent) {
		File parentFile = parent.getFilepath().toFile();
		FileTreeNode[] cached = this.childCache.get(parent.getFilepath());
		// If this node's content is already cached, return the cached children
		if (cached != null) {
			return cached;
		}
		FileTreeNode[] sortedChildren = {};
		File[] children = parentFile.listFiles(f -> f.canRead() && (f.isFile() || f.isDirectory()));
		if (children != null) {
			sortedChildren = Arrays.stream(children).map(f -> new FileTreeNode(f)).toArray(FileTreeNode[]::new);
		}
		if (sortedChildren.length > 0) {
			Arrays.sort(sortedChildren);
			this.childCache.clear();
			this.childCache.put(parent.getFilepath(), sortedChildren);
		}
		return sortedChildren;
	}

	/**
	 * Causes the view to query the model when the node representing
	 * {@code path} is displayed. This method must be used when a file has
	 * changed in the filesystem and that this change must be reflected in the
	 * tree.
	 * 
	 * @param path the Path to the file that has changed.
	 */
	public void fireTreeStructureChanged(Path path) {
		this.childCache.clear();
		TreePath treePath = makeTreePathFromPath(path.getParent());
		TreeModelEvent event = new TreeModelEvent(this, treePath);
		for (TreeModelListener listener : this.listeners) {
			listener.treeStructureChanged(event);
		}
	}

	/**
	 * Converts a Path object to a TreePath for use in the TreeModelEvent
	 * constructor.
	 * 
	 * @param path the Path to convert.
	 * @return a TreePath corresponding to {@code path}.
	 * @see TreeModelEvent#TreeModelEvent(Object, TreePath)
	 */
	private TreePath makeTreePathFromPath(Path path) {
		Stack<FileTreeNode> nodeStack = new Stack<FileTreeNode>();
		nodeStack.push(new FileTreeNode(path.toFile()));
		while (!path.equals(this.root.getFilepath())) {
			path = path.getParent();
			nodeStack.push(new FileTreeNode(path.toFile()));
		}
		TreePath treePath = new TreePath(nodeStack.pop());
		while (!nodeStack.isEmpty()) {
			treePath = treePath.pathByAddingChild(nodeStack.pop());
		}
		return treePath;
	}
}
